package com.corticon.dialogs.web.designer;

import static com.corticon.dialogs.model.PermissionType.SYS_ADMIN;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.io.PrintWriter;
import java.io.IOException;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;

import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import com.corticon.dialogs.dao.UserDAO;
import com.corticon.dialogs.dao.UserGroupDAO;
import com.corticon.dialogs.model.BasicGroup;
import com.corticon.dialogs.model.PermissionType;
import com.corticon.dialogs.model.User;
import com.corticon.dialogs.model.UserGroup;
import com.corticon.dialogs.util.Pager;
import com.corticon.dialogs.util.CommonSupport;
import com.corticon.dialogs.util.security.CustomUserDetails;
import com.corticon.dialogs.util.security.SecurityUtils;
import com.corticon.dialogs.validation.ValidationMessageFormatter;
import com.corticon.dialogs.validation.GenericHibernateValidator;
import com.corticon.dialogs.validation.designer.AdminUserValidator;
import com.corticon.dialogs.web.BaseController;
import com.corticon.dialogs.designer.licence.*;

/**
 * {@link SessionAttributes} annotation means these beans are session scope.
 * <p/>
 * TODO: Remember the orginal url.
 * TODO: Change the flag mark the type of operation.
 *
 * @author Yanglifan
 */
@Controller
@SessionAttributes({"user", "groups"})
public class UserController extends BaseController {

    // Request URIs.
    private final String CHANGE_STATUS_REQUEST = "changestatus";

    // View Names
    private final String LIST_VIEW_NAME = "designer.user.list";

    // Messages
    private String createUserSuccessMessage = "user.create.success";

    // DAOs
    @Resource
    private UserDAO userDAO;
    @Resource
    private UserGroupDAO userGroupDAO;
    @Resource
    private CommonSupport commonSupport;
    @Resource
    private LicenceManager licenceManager;

    // Other resources
    private CreateUserValidator createUserValidator;
    private EditUserValidator editUserValidator;
    private SimpleUserValidator simpleUserValidator;

    static String classPath = UserController.class.getResource("/").getPath();
    private static final String LICENSE_FILE_NAME = "DesignerLicense.lic";
    // Invalid license message, wrongly license file or license doesn't exist.
    private static final String INVALID_LICENSE = "Invalid designer license.";
    // Too many users
    private static final String EXCEEDED_MAX_USERS
        = "The number of users have exceed the current license's limitation. Please purchase new one.";
    // Exired date
    private static final String EXPIRED_DATE = "Your license is expired."; 



    // ********** Inner Classes ********** //
    /**
     * The validator for the user create.
     */
    public class CreateUserValidator extends AdminUserValidator<User> {

        public CreateUserValidator(ValidationMessageFormatter validationMessageFormatter) {
            super.validationMessageFormatter = validationMessageFormatter;
        }

        @Override
        public void complexValidate(Object object, Errors errors) {

            super.complexValidate(object, errors);

            // The type of the object has been checked in super class, so this cast is type safe.
            User user = (User) object;

            // Validate whether the username is already existed.
            User oldUser = userDAO.findById(user.getUsername());
            if (oldUser != null) {
                errors.rejectValue("username", "validate.user.username.duplicate",
                        "The username is already existed");
            }

            usernameValidate(user, errors);
        }

        /**
         * Validate whether the username involves invalid characters.
         *
         * @param user
         * @param errors
         */
        private void usernameValidate(User user, Errors errors) {
            if (!checkInvalidCharacters(user.getUsername(), ' ', '/', null) || !checkInvalidCharacters(
                    user.getUsername(), ':', '@', null) || !checkInvalidCharacters(user.getUsername(),
                    '[', '`', null) || !checkInvalidCharacters(user.getUsername(), '{', '~', null)) {

                errors.rejectValue("username", "validate.user.username.invalid",
                        "The username involves invalid characters.");
            }
        }
    }

    /**
     * The validator for the user edit.
     */
    public class EditUserValidator extends AdminUserValidator<User> {

        public EditUserValidator(ValidationMessageFormatter validationMessageFormatter) {
            super.validationMessageFormatter = validationMessageFormatter;
        }
    }

    public class SimpleUserValidator extends GenericHibernateValidator<User> {
        public SimpleUserValidator(ValidationMessageFormatter validationMessageFormatter) {
            super.validationMessageFormatter = validationMessageFormatter;
        }
    }

    /**
     * Instantiate the validators after the construct method.
     */
    @PostConstruct
    private void buildValidators() {
        createUserValidator = new CreateUserValidator(validationMessageFormatter);
        editUserValidator = new EditUserValidator(validationMessageFormatter);
        simpleUserValidator = new SimpleUserValidator(validationMessageFormatter);
    }

    private String validLicense() {
        String message = "";
        // Load the license file
        licenceManager.load(classPath + LICENSE_FILE_NAME);

        // Verify the signature of license
        if (!licenceManager.verify()) {
            message = INVALID_LICENSE;
        // Check the expired date
        } else if (!licenceManager.checkExpiredDate()) {
            message = EXPIRED_DATE;
        // Check the count of user.
        } else if (!licenceManager.checkMAXUser()) {
            message = EXCEEDED_MAX_USERS;
        }
        return message;
    }

    /**
     * The method is used to render the page before display the create user page. If you want to put
     * a model into requestScope, use method addAttribute in {@link ModelMap}.
     *
     * @param model
     * @return
     */
    @Transactional(readOnly = true)
    @RequestMapping(value = "create", method = RequestMethod.GET)
    public String getCreate(ModelMap model, HttpServletRequest request) {
//        if (!licenceManager.checkLicence()) {
//            String licenceMsg = "<font color='red'>You should purchase this product to support more users.</font>";
//            request.setAttribute("message", licenceMsg);
//            return getViewNameByRequestURI(request);
//        }
        String licenceMsg = validLicense();
        if ("".equals(licenceMsg)) {
            request.setAttribute("message", licenceMsg);
            return getViewNameByRequestURI(request);
        }

        setCreate(request);

        User user = new User();

        // Get the current admin object.
        CustomUserDetails currentAdmin = SecurityUtils.getCurrentUser();

        // The current admin is GROUP_ADMIN.
        PermissionType adminType = SecurityUtils.checkCurrentAdminPermission(currentAdmin);
        if (adminType == PermissionType.GROUP_ADMIN) {
            // 1 represent Group Admin.
            // Get the userGroup param from the currentAdmin.
            UserGroup userGroup = new UserGroup(currentAdmin.getGroupName());
            user.setUserGroup(userGroup);
        } else if (adminType == PermissionType.SYS_ADMIN) {
            // 2 represent Sys Admin.
            user.setUserGroup(new UserGroup());

            List<UserGroup> groups = userGroupDAO.findAll();
            List<String> groupNames = readUserGroupList(groups);
            model.addAttribute("groups", groupNames);
        }

        // Store the user obj into SessionScope model.
        model.addAttribute("user", user);

        // Convert the user permissions from Set to Map, then store it into SessionScope model.
        model.addAttribute("permissions", permissionsConverter(user.getPermissions()));

        request.setAttribute(DEFAULT_MESSAGE_KEY, getMessage("common.action.prompt"));

        // Read messages from SessionScope, put them into RequestScope, then clean messages from
        // SessionScope.
        readMessages(defaultMessageKeys, request);

        return getViewNameByRequestURI(request);
    }

    /**
     * Process the form SUBMIT of Create User Function.
     *
     * @param submitAction  The type of the action after SUBMIT, such as SUBMIT, SUBMIT then new, and
     *                      so on.
     * @param errors        {@link Errors} object, used by the spring validator.
     * @param sessionStatus {@link SessionStatus} used to control the status of the beans in session
     *                      scope.
     * @return
     */
    @Transactional
    @RequestMapping(value = CREATE_REQUEST, method = RequestMethod.POST)
    public String postCreate(@RequestParam String submitAction, @ModelAttribute User user,
                             Errors errors, ModelMap model, HttpServletRequest request, SessionStatus sessionStatus) {

        // Mark that this request is used for the user creation.
        setCreate(request);

        // Convert the user permissions from Set to Map.
        model.addAttribute("permissions", permissionsConverter(user.getPermissions()));

        // Validate the form object.
        Map<String, List<String>> errorsMessages = createUserValidator.invokeValidate(user,
                errors, request.getLocale());
        request.setAttribute("errors", errorsMessages);

        if (errors.hasErrors()) {
            return getViewNameByRequestURI(request);
        }

        // Set the Group property for this user.
        setUserGroup(user);

        User adminUser = commonSupport.findCurrentUser();
        user.setModifiedBy(adminUser);
        user.setModifiedOn(new Date());

        userDAO.save(user);

        sessionStatus.setComplete();
        request.getSession().removeAttribute("groups");

        // Add these messages into HttpSession.
        String[] args = {user.getUsername()};
        addMessages("message=" + createUserSuccessMessage, request, messageSource, args);

        return redirectBySubmitAction(submitAction, "username=" + user.getUsername());
    }

    @Transactional(readOnly = true)
    @RequestMapping(value = EDIT_REQUEST, method = RequestMethod.GET)
    public String getEdit(@RequestParam String username, ModelMap model, HttpServletRequest request) {

        setEdit(request);

        User user = userDAO.findById(username);

        // If the user is not existed, display the message.
        if (user == null) {
            // TODO: i18n.
            request.setAttribute("message", "The user " + "(" + username + ") is not existed.");
            return errorsViewName;
        }

        if (!permissionCheck(user)) {
            return redirectTo403();
        }

        // Disabled user cannot be edited.
        if (!user.isEnabled()) {
            String[] args = {username};
            addMessages("message=user.edit.disabled", request, messageSource, args);
            return redirectPrefix + LIST_REQUEST + requestSuffix;
        }

        CustomUserDetails currentAdmin = SecurityUtils.getCurrentUser();

        if (SecurityUtils.checkCurrentAdminPermission(currentAdmin) == PermissionType.SYS_ADMIN) {
            // Read all user groups, then set into model.
            List<UserGroup> groups = userGroupDAO.findAll();
            List<String> groupNames = readUserGroupList(groups);
            model.addAttribute("groups", groupNames);
        }

        user.setConfirmPassword(user.getPassword());
        model.addAttribute("user", user);

        // Convert the user permissions from Set to Map.
        model.addAttribute("permissions", permissionsConverter(user.getPermissions()));

        request.setAttribute(DEFAULT_MESSAGE_KEY, getMessage("common.action.prompt"));

        readMessages(defaultMessageKeys, request);

        return getViewNameByRequestURI(request);
    }

    @Transactional
    @RequestMapping(value = EDIT_REQUEST, method = RequestMethod.POST)
    public String postEdit(@RequestParam String submitAction, @ModelAttribute User user, Errors errors, ModelMap model,
                           SessionStatus sessionStatus, HttpServletRequest request) {

        // Mark that this request is used for the user update.
        setEdit(request);

        // Convert the user permissions from Set to Map.
        model.addAttribute("permissions", permissionsConverter(user.getPermissions()));

        // Validate the form object.
        Map<String, List<String>> errorsMessages = editUserValidator.invokeValidate(user, errors, request.getLocale());
        request.setAttribute("errors", errorsMessages);

        // If there are errors, return to the original page.
        if (errors.hasErrors()) {
            return getViewNameByRequestURI(request);
        }

        // Set the Group property for this user.
        setUserGroup(user);

        User adminUser = commonSupport.findCurrentUser();
        user.setModifiedBy(adminUser);
        user.setModifiedOn(new Date());

        userDAO.update(user);

        sessionStatus.setComplete();

        // Add message.
        String[] args = {user.getUsername()};
        addMessages("message=user.edit.success", request, messageSource, args);

        return redirectBySubmitAction(submitAction, "username=" + user.getUsername(), request);
    }

    @Transactional(readOnly = true)
    @RequestMapping(value = CHANGE_STATUS_REQUEST, method = RequestMethod.GET)
    public String getChangeStatus(@RequestParam String username, ModelMap model, HttpServletRequest request) {

        User user = userDAO.findById(username);

        // If the user is not existed, display the message.
        if (user == null) {
            request.setAttribute("message", "The user " + "(" + username + ") is not existed.");
            return errorsViewName;
        }

        if (!permissionCheck(user)) {
            return redirectTo403();
        }

        // An admin can not disabled himself.
        if (SecurityUtils.getCurrentUser().getUsername().equals(user.getUsername())) {
            addMessages("message=user.enable.self", request, messageSource, null);
            return redirectPrefix + LIST_REQUEST + requestSuffix;
        }

        model.addAttribute("user", user);

        request.setAttribute(DEFAULT_MESSAGE_KEY, getMessage("common.action.prompt"));

        return getViewNameByRequestURI(request);
    }

    @Transactional
    @RequestMapping(value = CHANGE_STATUS_REQUEST, method = RequestMethod.POST)
    public String postChangeStatus(@ModelAttribute User user, Errors errors, HttpServletRequest request, SessionStatus sessionStatus) {

        // If to disable a user, the disabled note is necessary.
        if (!user.isEnabled() && StringUtils.isEmpty(user.getDisabledNote())) {
            request.setAttribute("disabledNoteError",
                    "The disabled notes is necessary, while disabling a user.");
            return getViewNameByRequestURI(request);
        }

        // Validate the form object.
        Map<String, List<String>> errorsMessages = simpleUserValidator.invokeValidate(user, errors, request.getLocale());
        request.setAttribute("errors", errorsMessages);

        // If there are errors, return to the original page.
        if (errors.hasErrors()) {
            return getViewNameByRequestURI(request);
        }

        // If the operation is to disable a user, set the disabledBy and disabledOn properties.
        if (!user.isEnabled()) {
            CustomUserDetails currentAdmin = SecurityUtils.getCurrentUser();
            user.setDisabledBy(currentAdmin.getUsername());
            user.setDisabledOn(new Date());
        }

        userDAO.update(user);
        sessionStatus.setComplete();

        // Add operation success message.
        if (user.isEnabled()) {
            String enableUserMessage = "message=user.enable";
            addMessages(enableUserMessage, request, messageSource, null);
        } else {
            String disableUserMessage = "message=user.disable";
            addMessages(disableUserMessage, request, messageSource, null);
        }

        return redirectPrefix + LIST_REQUEST + requestSuffix;
    }

    @Transactional(readOnly = true)
    @RequestMapping(value = "view", method = RequestMethod.GET)
    public String view(@RequestParam String username, HttpServletRequest request) {

        User user = userDAO.findById(username);

        // If the user is not existed, display the message.
        if (user == null) {
            request.setAttribute("message", "The user " + "(" + username + ") is not existed.");
            return errorsViewName;
        }

        if (!permissionCheck(user)) {
            return redirectTo403();
        }

        Map<String, Boolean> permissions = permissionsConverter(user.getPermissions());
        request.setAttribute("permissions", permissions);

        request.setAttribute("user", user);
        request.setAttribute("currentUser", SecurityUtils.getCurrentUser().getUsername());

        readMessages(defaultMessageKeys, request);
        return getViewNameByRequestURI(request);
    }

    /**
     * List users according to the pagination infos.
     *
     * @param page  The current page number.
     * @param model
     * @return
     */
    @Transactional(readOnly = true)
    @RequestMapping(method = RequestMethod.GET)
    public String list(@RequestParam(required = false) Integer page, ModelMap model, HttpServletRequest request) {

        CustomUserDetails currentAdmin = SecurityUtils.getCurrentUser();

        User userExample = new User();

        // If the current admin is a group admin.
        if (SecurityUtils.checkCurrentAdminPermission(currentAdmin) == PermissionType.GROUP_ADMIN) {
            UserGroup currentUserGroup = userGroupDAO.load(currentAdmin.getGroupName());
            userExample.setUserGroup(currentUserGroup);
        }

        Pager pager = constructPager(page, request);
        long count = userDAO.getCountByExample(userExample, "enabled");
        pager.setNumOfRecords(count);
        pager = setPagerSort(pager, "username", request);

        List<User> users = userDAO.getPageOfDataByExample(userExample, pager, "enabled");

        // Put the pager object into the model.
        request.setAttribute("pager", pager);

        if (users.size() > 0) {
            request.setAttribute("users", users);
        }

        // Convert the permissions info in the user object into the format 'p1, p2'.
        getPermissionsInfo(users);

        // Load groups info.
        List<UserGroup> groups = userGroupDAO.findAll();
        request.setAttribute("groups", groups);
        if (users != null && users.size() > 0) {
            request.setAttribute("foundMessage", getMessage(String.valueOf(pager.getNumOfRecords()),
                    publicFoundMessage));
        } else {
            request.setAttribute("foundMessage", getMessage("", publicNotFoundMessage));
        }
        readMessages(defaultMessageKeys, request);

        return getViewNameByRequestURI(request);
    }

    @Transactional(readOnly = true)
    @RequestMapping(method = RequestMethod.GET)
    public String search(@RequestParam(required = false) Integer page, @RequestParam(required =
            false) String keywords, @RequestParam(required = false) String groupName, @RequestParam(
            required = false) Boolean enabled, HttpServletRequest request) {

        // Get the permissions and the instance of the current admin.
        List<String> permissions = SecurityUtils.getCurrentUserPermissions();
        CustomUserDetails currentAdmin = SecurityUtils.getCurrentUser();

        // The group admin can only search the users have the same group with him.
        if (permissions.contains(PermissionType.GROUP_ADMIN.value())) {
            if (StringUtils.isNotEmpty(groupName) && !currentAdmin.getGroupName().equals(groupName)) {
                return redirectTo403();
            } else {
                groupName = currentAdmin.getGroupName();
            }
        }

        // Construct the pager object.
        Pager pager = constructPager(page, request);
        pager.setNumOfRecords(userDAO.getSearchCount(keywords, groupName, enabled));
        pager = setPagerSort(pager, "username", request);

        String enabledString = enabled == null ? "" : enabled.toString();
        groupName = groupName == null ? "" : groupName;

        pager.setRequestParameters(pager.getRequestParameters() + "&keywords=" + keywords +
                "&enabled=" + enabledString + "&groupName=" + groupName);
        // Put the pager object into the model.
        request.setAttribute("pager", pager);

        // Search users and set them into the request scope.
        List<User> users = new ArrayList<User>();
        users = userDAO.search(keywords, groupName, enabled, pager);
        request.setAttribute("users", users);

        if (permissions.contains(PermissionType.SYS_ADMIN.value())) {
            //get all groups for search select
            List<UserGroup> groups = userGroupDAO.findAll();
            request.setAttribute("groups", groups);
        }

        // Convert the permissions info in the user object into the format 'p1, p2'.
        getPermissionsInfo(users);
        if (users != null && users.size() > 0) {
            request.setAttribute("foundMessage", getMessage(String.valueOf(pager.getNumOfRecords()),
                    publicFoundMessage));
        } else {
            request.setAttribute("foundMessage", getMessage("", publicNotFoundMessage));
        }
        return LIST_VIEW_NAME;
    }

    // ********** Helpers ********** //
    /**
     * Convert the user permission infos into Map format.
     *
     * @param permissions
     * @return
     */
    private Map<String, Boolean> permissionsConverter(Set<String> permissions) {

        Assert.notNull(permissions);

        Map<String, Boolean> permissionsMap = new HashMap<String, Boolean>();

        permissionsMap.put(PermissionType.AUTHOR.value(), false);
        permissionsMap.put(PermissionType.GROUP_ADMIN.value(), false);
        permissionsMap.put(PermissionType.PORTAL_USER.value(), false);
        permissionsMap.put(PermissionType.REVIEWER.value(), false);
        permissionsMap.put(PermissionType.SYS_ADMIN.value(), false);

        for (String permission : permissions) {
            permissionsMap.put(permission, true);
        }

        return permissionsMap;
    }

    /**
     * Check whether the current admin has the permission to operate a user.
     *
     * @param user
     * @return
     */
    private boolean permissionCheck(User user) {
        CustomUserDetails currentAdmin = SecurityUtils.getCurrentUser();

        // If the current admin is a group admin.
        if (SecurityUtils.checkCurrentAdminPermission(currentAdmin).equals(
                PermissionType.GROUP_ADMIN)) {
            // A group admin cannot operate a sys admin.
            if (user.getPermissions().contains(SYS_ADMIN.value())) {
                return false;
            } else if (!currentAdmin.getGroupName().equals(user.getUserGroup().getName())) {
                // A group admin cannot operate a user in the other group.
                return false;
            }
        }

        return true;
    }

    /**
     * Load a proxy of a user group according the name of the user group. Then set the user group
     * into the user.
     *
     * @param user
     */
    private void setUserGroup(User user) {
        // Load the user group.
        UserGroup userGroup = userGroupDAO.load(user.getUserGroup().getName());
        user.setUserGroup(userGroup);
    }

    /**
     * Get the permission infos for all of users. The permission infos will be set into the property
     * 'permissionsInfo' for each of users. The permissionsInfo is in the format 'permission1, permission2',
     * displayed on the list page and the search result page. The name of this method is not good enough,
     * but I cannot find a better name.
     *
     * @param users
     */
    private void getPermissionsInfo(List<User> users) {
        for (User user : users) {
            int i = 1;
            StringBuffer infos = new StringBuffer();
            for (String permission : user.getPermissions()) {
                if (permission.equals(PermissionType.AUTHOR.value())) {
                    infos.append("Author");
                }
                if (permission.equals(PermissionType.GROUP_ADMIN.value())) {
                    infos.append("Group Administrator");
                }
                if (permission.equals(PermissionType.PORTAL_USER.value())) {
                    infos.append("Portal User");
                }
                if (permission.equals(PermissionType.REVIEWER.value())) {
                    infos.append("Reviewer");
                }
                if (permission.equals(PermissionType.SYS_ADMIN.value())) {
                    infos.append("System Administrator");
                }
                if (i != user.getPermissions().size()) {
                    infos.append(", ");
                    i++;
                }
            }
            user.setPermissionsInfo(infos.toString());
        }
    }

    /**
     * Read the user groups info into a String list. Filter out the "ALL" and "PORTAL" groups.
     *
     * @param userGroups
     */
    private List<String> readUserGroupList(List<UserGroup> userGroups) {
        List<String> groupNames = new ArrayList<String>();
        for (UserGroup group : userGroups) {
            if (group.getName().equals(BasicGroup.ALL.toString()) || group.getName().
                    equalsIgnoreCase(BasicGroup.PORTAL.toString())) {
                continue;
            }
            groupNames.add(group.getName());
        }

        return groupNames;
    }

    // ********** Setters ********** //
    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

    public void setUserGroupDAO(UserGroupDAO userGroupDAO) {
        this.userGroupDAO = userGroupDAO;
    }
}
